package physics;

import java.awt.Graphics2D;
import java.awt.Point;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import physics.component.Mass;
import physics.component.PhysicalObject;
import physics.component.Spring;
import physics.globalforce.GlobalForce;
import ui.Drawable;


/**
 * A collection of physical objects, that as a whole constitute an "assembly" of
 * components.
 *
 * @author Kevin Gao, Connor Gordon
 *
 */
public class Assembly implements Drawable {

    private final List<PhysicalObject> myComponents;
    private final Simulation mySimulation;
    private double myTotalMass;

    /**
     * Constructor for an assembly of components.
     *
     * @param s The containing simulation that the assembly runs in.
     */
    public Assembly (final Simulation s) {
        mySimulation = s;
        myComponents = new ArrayList<PhysicalObject>();
        myTotalMass = 0;
    }

    @Override
    public final void paint (final Graphics2D pen) {
        for (PhysicalObject po : myComponents) {
            po.paint(pen);
        }
    }

    /**
     * Method for adding components to the assembly. If component is a mass,
     * update total mass of assembly.
     *
     * @param po Component to add
     */
    public final void add (final PhysicalObject po) {
        myComponents.add(po);
        Collections.sort(myComponents, new AssemblyComponentComparator());
        if (po instanceof Mass) {
            myTotalMass += ((Mass) po).getMass();
        }
    }

    /**
     * Updates all the springs and masses in the simulation. Note that springs
     * must be updated first so that the forces on the mass are correct when
     * they are updated.
     *
     * @param dt Change in time
     */
    public final void updateMovers (final double dt) {
        for (PhysicalObject po : myComponents) {
            po.update(mySimulation, dt);
        }
    }

    /**
     * Returns the closest mass within the assembly relative to a given mass.
     *
     * @param relativeMass Mass to be measured against
     * @return Mass in the assembly that's closest to the one given
     */
    public final Mass getClosestMass (final Mass relativeMass) {
        double closestDistance = Double.MAX_VALUE;
        Mass closestMass = null;
        for (Mass m : getMasses()) {
            double newDistance = m.getDistanceBetween(relativeMass);
            if (newDistance < closestDistance) {
                closestDistance = newDistance;
                closestMass = m;
            }
        }
        return closestMass;
    }

    /**
     * Checks whether or not an assembly contains a component.
     *
     * @param po PhysicalObject in question
     * @return Boolean telling whether or not the component is in within the
     *         assembly
     */
    public final boolean contains (final PhysicalObject po) {
        return myComponents.contains(po);
    }

    /**
     * Returns the object reference to the mass using the given id.
     *
     * @param id The mass's id number
     * @return Mass object of given id
     */
    public final Mass getMass (final int id) {
        for (Mass m : getMasses()) {
            if (m.match(id)) { return m; }
        }
        return null;
    }

    /**
     * Removes a given component from the assembly if it exists. If component is
     * a mass, remove mass from total mass.
     *
     * @param po Component to remove
     * @return true if mass was removed, false if not found
     */
    public final boolean remove (final PhysicalObject po) {
        if (po instanceof Mass) {
            myTotalMass -= ((Mass) po).getMass();
        }
        return myComponents.remove(po);
    }

    /**
     * Computes the center of mass for an assembly.
     *
     * @return Point corresponding to center of mass
     */
    public final Point getCenterOfMass () {
        double centerX = 0;
        double centerY = 0;

        for (PhysicalObject po : myComponents) {
            if (!(po instanceof Mass)) {
                continue;
            }
            Mass currentMass = (Mass) po;
            centerX += currentMass.getCenter().getX() * currentMass.getMass();
            centerY += currentMass.getCenter().getY() * currentMass.getMass();
        }

        centerX /= myTotalMass;
        centerY /= myTotalMass;
        return new Point((int) centerX, (int) centerY);
    }

    /**
     * Getter for myTotalMass.
     *
     * @return Total mass in assembly
     */
    public final double getTotalMass () {
        return myTotalMass;
    }

    /**
     * Given a global force, this method will apply it to all of an assembly's
     * masses.
     *
     * @param gf GlobalForce to be applied
     */
    public final void applyGlobalForce (final GlobalForce gf) {
        // Apply global force if its type is enabled and the particular force is
        // enabled
        if (EnvironmentSingleton.getInstance().getForceState(gf.getForceType())
                && gf.isEnabled()) {
            for (Mass m : getMasses()) {
                gf.applyForce(m, this);
            }
        }
    }

    /**
     * Filters out non-masses from components and returns list of masses.
     *
     * @return List of masses.
     */
    private List<Mass> getMasses () {
        ArrayList<Mass> masses = new ArrayList<Mass>();
        for (PhysicalObject po : myComponents) {
            if (Mass.isMass(po)) {
                masses.add((Mass) po);
            }
        }
        return masses;
    }

    /**
     * Comparator used to sort springs before masses. This ensures that the sort
     * order maintains that springs come before masses. More precise sorting is
     * not necessary at this stage.
     */
    private class AssemblyComponentComparator implements
            Comparator<PhysicalObject> {

        @Override
        public int compare (PhysicalObject po1, PhysicalObject po2) {
            boolean po1IsSpring = po1 instanceof Spring;
            boolean po1IsMass = po1 instanceof Mass;
            boolean po2IsSpring = po2 instanceof Spring;
            boolean po2IsMass = po2 instanceof Mass;
            // Implements the following "truth" table
            /*
             * po1 | po2 | res
             * -----------------
             * - | - | 0
             * s | - | 0
             * m | - | 0
             * - | s | 0
             * s | s | 0
             * m | s | 1
             * - | m | 0
             * s | m | -1
             * m | m | 0
             */
            if (po1IsMass && po2IsSpring) { return 1; }
            if (po1IsSpring && po2IsMass) { return -1; }
            return 0;
        }
    }

}
